Skip to content

feat(agent): cumulative prompt/completion token-split accessors#1

Open
indykish wants to merge 1 commit into
release/v2026.5.29-zmbfrom
patch/split-token-accessors-v2026.5.29
Open

feat(agent): cumulative prompt/completion token-split accessors#1
indykish wants to merge 1 commit into
release/v2026.5.29-zmbfrom
patch/split-token-accessors-v2026.5.29

Conversation

@indykish

@indykish indykish commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Carries one commit atop upstream tag v2026.5.29: the Agent already normalizes prompt_tokens/completion_tokens per response but folds only total_tokens. This accumulates both splits at the turn and summary-compaction sites and exposes promptTokensUsed()/completionTokensUsed() beside the existing tokensUsed(), so an embedder can meter prompt-side and completion-side spend separately without re-deriving usage from history.

Why a fork

usezombie bills platform-model token spend by side and needs these accessors. We hold no push/release rights on upstream nullclaw/nullclaw, so the patch rides this fork (usezombie/nullclaw) until upstream exposes equivalent accessors — at which point the consumer pin returns to the upstream tag and this fork is dropped.

Changes

  • prompt_tokens_total / completion_tokens_total cumulative fields on Agent (default 0).
  • Accumulation at both fold sites (per-turn response + summary compaction).
  • promptTokensUsed() / completionTokensUsed() accessors.
  • The existing Agent tokens tracking test extended to cover the splits.

Verification

zig build test --summary all — the pristine tag and this patched branch fail only the same 5 pre-existing redis-environment integration tests (those dial localhost:6379, which on a dev box is an unrelated container; clean in upstream CI). No new failures. Released as v2026.5.29-zmb.1.

Greptile Summary

Adds cumulative prompt_tokens_total / completion_tokens_total fields to Agent, folds them at both the per-turn response and summary-compaction sites, and exposes promptTokensUsed() / completionTokensUsed() alongside the existing tokensUsed() so embedders can meter by token side. The clearSessionState reset and /new-command test are also updated.

  • New fields & accessors (root.zig): two u64 fields default to 0, accumulated at the same two fold-sites as total_tokens, exposed via typed accessors.
  • Session reset (commands.zig): clearSessionState gains @hasField-guarded zeroing of both new fields, mirroring the existing total_tokens pattern; tested by an extended /new assertion in root.zig.

Confidence Score: 4/5

The change is safe to merge for non-billing consumers; the billing-accuracy gap for providers that report total but not per-side tokens should be addressed before production metering is switched on.

The accumulation logic is correct for all providers that supply split data, and the session-reset path is handled cleanly. The one real concern is that neither normalization block back-fills split tokens when a provider gives total_tokens without per-side data — those turns leave prompt_tokens_total and completion_tokens_total at zero while total_tokens advances, so promptTokensUsed() + completionTokensUsed() can silently diverge from tokensUsed(). For the stated purpose of by-side billing metering, that gap is a current defect on the hot path.

src/agent/root.zig — the accumulation sites at lines 2443–2445 and 2864–2865 need a guard or documented caveat for the case where a provider omits per-side token counts.

Important Files Changed

Filename Overview
src/agent/root.zig Adds prompt_tokens_total/completion_tokens_total fields, accumulates them at both response-fold sites, exposes promptTokensUsed()/completionTokensUsed() accessors, and extends tests. Correct when providers supply per-side data, but silently stays zero when a provider reports total_tokens without splits.
src/agent/commands.zig Adds @hasField-guarded resets for the two new fields inside clearSessionState, matching the existing pattern for total_tokens. Correct and consistent.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[LLM Response received] --> B[Normalize TokenUsage]
    B --> C{total==0 and splits present?}
    C -- Yes --> D[total = prompt + completion]
    C -- No --> E{total==0 and all zero and text present?}
    D --> F[Fold into Agent counters]
    E -- Yes --> G[completion = estimated tokens\ntotal = estimated tokens]
    E -- No --> F
    G --> F
    F --> H[total_tokens accumulates]
    F --> I[prompt_tokens_total accumulates]
    F --> J[completion_tokens_total accumulates]
    H --> K[tokensUsed]
    I --> L[promptTokensUsed]
    J --> M[completionTokensUsed]
    C -- No --> W{total greater than 0 but splits zero?}
    W -- Yes --> X[splits stay 0 - silent gap]
    X --> F
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/agent/root.zig:2443-2445
**Split counters silently zero-out when provider omits per-side data**

When a provider sets `total_tokens > 0` but leaves `prompt_tokens = 0` and `completion_tokens = 0` (neither normalization block fires because `total_tokens` is already non-zero), the accumulators for `prompt_tokens_total` and `completion_tokens_total` receive 0 for that turn while `total_tokens` advances correctly. Over multiple such turns, `promptTokensUsed() + completionTokensUsed()` falls short of `tokensUsed()` with no signal to the caller. Since the stated purpose of these accessors is by-side billing, an embedder that relies on them for cost metering will silently under-charge prompt-side and completion-side spend for any provider that only surfaces a total — a hard-to-detect billing gap.

Reviews (2): Last reviewed commit: "feat(agent): accumulate prompt/completio..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

indykish added a commit to agentsfleet/agentsfleet that referenced this pull request Jun 12, 2026
…h commit

The fork patch is now reviewed (agentsfleet/nullclaw#1) and released as
tag v2026.5.29-zmb.1 at the same commit (127b5ac4). Swapped the pin
ref from the branch name to the release tag — same commit, so the
content hash is byte-identical (verified via zig fetch), a more
semantic + immutable reference. Spec §1 + Depends-on updated to cite
the release. Both build graphs compile against the tag pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/agent/root.zig
…e accessors

The agent already normalizes prompt_tokens/completion_tokens per response
but folds only total_tokens. Accumulate both splits at the turn and
summary-compaction sites and expose promptTokensUsed()/completionTokensUsed()
beside tokensUsed(), so embedders can meter prompt-side and completion-side
spend separately without re-deriving usage from history.

clearSessionState() zeroes both new counters alongside total_tokens
(@hasField-guarded, matching the existing pattern), so /new and /reset
don't leave by-side metering carrying stale per-session figures while
tokensUsed() reads 0. The "/new clears history" test asserts it.

Carried on this fork atop v2026.5.29 until upstream exposes split accessors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@indykish indykish force-pushed the patch/split-token-accessors-v2026.5.29 branch from 127b5ac to dd85a21 Compare June 12, 2026 03:57
Comment thread src/agent/root.zig
Comment on lines 2443 to +2445
self.total_tokens += normalized_usage.total_tokens;
self.prompt_tokens_total += normalized_usage.prompt_tokens;
self.completion_tokens_total += normalized_usage.completion_tokens;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Split counters silently zero-out when provider omits per-side data

When a provider sets total_tokens > 0 but leaves prompt_tokens = 0 and completion_tokens = 0 (neither normalization block fires because total_tokens is already non-zero), the accumulators for prompt_tokens_total and completion_tokens_total receive 0 for that turn while total_tokens advances correctly. Over multiple such turns, promptTokensUsed() + completionTokensUsed() falls short of tokensUsed() with no signal to the caller. Since the stated purpose of these accessors is by-side billing, an embedder that relies on them for cost metering will silently under-charge prompt-side and completion-side spend for any provider that only surfaces a total — a hard-to-detect billing gap.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent/root.zig
Line: 2443-2445

Comment:
**Split counters silently zero-out when provider omits per-side data**

When a provider sets `total_tokens > 0` but leaves `prompt_tokens = 0` and `completion_tokens = 0` (neither normalization block fires because `total_tokens` is already non-zero), the accumulators for `prompt_tokens_total` and `completion_tokens_total` receive 0 for that turn while `total_tokens` advances correctly. Over multiple such turns, `promptTokensUsed() + completionTokensUsed()` falls short of `tokensUsed()` with no signal to the caller. Since the stated purpose of these accessors is by-side billing, an embedder that relies on them for cost metering will silently under-charge prompt-side and completion-side spend for any provider that only surfaces a total — a hard-to-detect billing gap.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant